# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import logging

from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway import OrderSide, OrderType


class BitmexLazyPullMMExecutor(object):
  def __init__(self, execution_params, order_gateway, logger=None):
    self._logger = logger or logging.getLogger('BitmexLazyPullMMExecutor')
    self._og = order_gateway

    self._product = execution_params['product']
    self._native_symbol = execution_params['product'].native_symbol

    self._lot_size = execution_params['lot_size']
    self._min_pos = execution_params['min_pos']
    self._max_pos = execution_params['max_pos']
    self._order_update_period = (execution_params.get('order_update_period', 2.) * (10**9))
    self._post_only = execution_params.get('post_only', True)
    self._pull_threshold = execution_params['pull_threshold_bp'] / 10000.

    self._pos = 0
    self._sell_on_the_fly_orders = []
    self._sell_working_orders = []
    self._buy_on_the_fly_orders = []
    self._buy_working_orders = []

    self._last_fire_time = 0
    self._create_list = []
    self._amend_list = []

  def _update_orders(self):
    self._pos = self._og.get_position(self._product)

    self._sell_on_the_fly_orders = []
    self._sell_working_orders = []
    self._buy_on_the_fly_orders = []
    self._buy_working_orders = []

    for order in self._og.get_working_order():
      if order.product != self._native_symbol:
        continue

      if order.order_side == OrderSide.SELL:
        if order.accepted:
          self._sell_working_orders.append(order)
        else:
          self._sell_on_the_fly_orders.append(order)

      elif order.order_side == OrderSide.BUY:
        if order.accepted:
          self._buy_working_orders.append(order)
        else:
          self._buy_on_the_fly_orders.append(order)

      else:
        self._logger.error('Unknown side: %s', str(order))
        continue

    self._sell_working_orders = sorted(self._sell_working_orders,
                                       key=lambda order: order.price,
                                       reverse=False)
    self._buy_working_orders = sorted(self._buy_working_orders,
                                      key=lambda order: order.price,
                                      reverse=True)

  def _cancel_sell_working_orders(self, exclude_order_id=None):
    for order in self._sell_working_orders:
      if order.order_id == exclude_order_id:
        continue
      self._amend_list.append(self._og.make_bitmex_amend(order.order_id, leaves_qty=0))

  def _cancel_buy_working_orders(self, exclude_order_id=None):
    for order in self._buy_working_orders:
      if order.order_id == exclude_order_id:
        continue
      self._amend_list.append(self._og.make_bitmex_amend(order.order_id, leaves_qty=0))

  def _manage_sell_order(self, sell_price):
    # If there is an on-the-fly order, cancel all working orders and keep the
    # on-the-fly order.
    if (sell_price is None or self._sell_on_the_fly_orders or self._pos <= self._min_pos):
      self._cancel_sell_working_orders()
      return

    # Create new order if there is no working order.
    if not self._sell_working_orders:
      order = self._og.make_bitmex_order(self._native_symbol,
                                         sell_price,
                                         self._lot_size,
                                         OrderSide.SELL,
                                         OrderType.LIMIT,
                                         post_only=self._post_only)
      self._create_list.append(order)
      self._logger.info('SELL %s %f@%.1f', self._native_symbol, self._lot_size, sell_price)
      return

    # If there is an working order that its price is same to sell_price,
    # keep it and cancel all other orders.
    min_keep_price = sell_price * (1. - self._pull_threshold)
    for order in self._sell_working_orders:  # price-ascending order.
      if min_keep_price <= order.price and order.price <= sell_price:
        self._cancel_sell_working_orders(exclude_order_id=order.order_id)
        return

    # Choose one with the smallest price and modify it and
    # cancel all other orders.
    chosen_order = self._sell_working_orders[0]
    self._cancel_sell_working_orders(exclude_order_id=chosen_order.order_id)
    self._amend_list.append(
        self._og.make_bitmex_amend(chosen_order.order_id,
                                   price=sell_price,
                                   leaves_qty=self._lot_size))
    self._logger.info('SELL %s %f@%.1f', self._native_symbol, self._lot_size, sell_price)

  def _manage_buy_order(self, buy_price):
    # If there is an on-the-fly order, cancel all working orders and keep the
    # on-the-fly order.
    if (buy_price is None or self._buy_on_the_fly_orders or self._pos >= self._max_pos):
      self._cancel_buy_working_orders()
      return

    # Create new order if there is no working order.
    if not self._buy_working_orders:
      order = self._og.make_bitmex_order(self._native_symbol,
                                         buy_price,
                                         self._lot_size,
                                         OrderSide.BUY,
                                         OrderType.LIMIT,
                                         post_only=self._post_only)
      self._create_list.append(order)
      self._logger.info('BUY  %s %f@%.1f', self._native_symbol, self._lot_size, buy_price)
      return

    # If there is an working order that its price is same to buy_price,
    # keep it and cancel all other orders.
    max_keep_price = buy_price * (1. + self._pull_threshold)
    for order in self._buy_working_orders:  # price-descending order
      if buy_price <= order.price and order.price <= max_keep_price:
        self._cancel_buy_working_orders(exclude_order_id=order.order_id)
        return

    # Choose one with the biggest price and modify it and
    # cancel all other orders.
    chosen_order = self._buy_working_orders[0]
    self._cancel_buy_working_orders(exclude_order_id=chosen_order.order_id)
    self._amend_list.append(
        self._og.make_bitmex_amend(chosen_order.order_id,
                                   price=buy_price,
                                   leaves_qty=self._lot_size))
    self._logger.info('BUY  %s %f@%.1f', self._native_symbol, self._lot_size, buy_price)

  def manage_mm_orders(self, sell_price, buy_price):
    if get_timestamp() - self._last_fire_time < self._order_update_period:
      return False

    self._create_list = []
    self._amend_list = []
    self._update_orders()

    self._manage_sell_order(sell_price)
    self._manage_buy_order(buy_price)

    fired = False
    if self._amend_list:
      self._og.amend_bulk(self._amend_list)
      self._logger.debug('Amend %d orders', len(self._amend_list))
      fired = True
    if self._create_list:
      self._og.submit_bulk(self._create_list)
      self._logger.debug('Create %d orders', len(self._create_list))
      fired = True

    if fired:
      self._last_fire_time = get_timestamp()
    return fired
